
JS 的 Iteration 在 Slate 裡頭佔了不小的份量,即便有 Ref concepts 讓我們得以追蹤特定的 Location value ,在很多時候我們仍會需要透過『遍歷』的方式去實現我們的功能。
在 interfaces/ 裡提供的所有 method apis ,只要名稱是『複數( s 結尾的那種 )』,有 87% 都是透過遍歷的方式實作,而它們除了提供給開發者使用之外也很大幅度地被 Transforms 或 Operations 使用。
我們一樣依照慣例先來介紹 JS 裡與 Iteration 的先備知識後再回頭介紹 Slate 是如何實作這件事的
我們首先從 JS 的迭代協議介紹起,在 ES6 的補充內容中,共有兩個針對迭代功能的協議,分別是:可迭代協議( Iterable protocol )、迭代器協議( Iterator protocol )
光是看名字實在很難分辨清楚這兩個 protocols 到底有什麼差異,讓我們依序來介紹它們。
可迭代協議( Iterable protocol )
這個協議用途是『定義有哪些 JS Object 是可以迭代的』,只有滿足這項協議的 Objects 才能進入 for..of statement 的大門,迭代裡頭的 value
var a = { first: 1, second: 2 }
for (let i of a) {
	console.log('success', i) // Uncaught TypeError: a is not iterable
}
var b = [1, 2]
for (let i of b) {
	console.log('success', i) // success 1 \n success 2
}
附上 for...of 在 MDN 上的解釋:『 The for...of statement creates a loop iterating over iterable objects 』
想一想其實也蠻合理的,如果沒有這個協議提供給 for...of ,它又該如何判斷丟進來的 value 能不能做迭代呢?就算沒有協議規範,上例的 code 裡頭的 a 也很明顯不是一個能直接拿來迭代的 object 。
理解這個協議的用途後,我們又應該如何實作出一個 iterable 的 object 呢?
我們可以從已經符合協議規範的 Array 中找到一些線索:
Array.prototype[Symbol.iterator]() // Array Iterator {}
實作的方式就是在 object (或在原型鏈的原型物件)中實作一個擁有 @@iterator key 的 method ,而 @@iterator 就被存在 ES6 的 Symbol.iterator 的回傳值中:
var a = {
	...,
	[Symbol.iterator]: ... // method implementation
}
丟入 for...of statement 以後它就會自動去搜尋 @@iterator key 回傳的 method 。
還有另一個限制就是 @@iterator method 必須要回傳一組符合下一個迭代器協議的 Iterator 。
迭代器協議( Iterator protocol )
這個協議定義了一個迭代器( Iterator )所應具備的內容。
一個符合規範的迭代器必須要具有 next key 的 method ,而它首先必須不能接受任何參數,另外它必須要回傳一組至少擁有下方兩個屬性的物件:
done (布林值)
true 代表已迭代完畢整個序列,此時回傳的 object 可以只包含 done 就好false 代表還沒迭代完畢,迭代器仍能產出序列中的下一個值value
當前回傳的成員值。
只要符合規範,我們可以任意定義整組迭代器的內容,我們試著來實作看看
const iteratorEx = {
  start: 1,
  end: 3,
  [Symbol.iterator]() {
    this.current = this.start;
    return this;
  },
  next() {
    if (this.current >= 1 && this.current <= 3) {
      return { done: false, value: this.current++ };
    }
    return { done: true };
  },
};
for (let num of iteratorEx) {
  console.log(num); // 1\n2\n3
}
iteratorEx 物件,將它丟進 for...of statement 時它首先會找到並呼叫 @@iterator key 的 method@@iterator method 裡頭的 this 指向 iteratorEx ,建立 current key 並將 value 指向 start 的位置並回傳 iteratorEx 本身@@iterator method 回傳的結果,也就是 iteratorEx 裡頭的 next method ,依照裡面定義的邏輯去做迭代並輸出結果。Protocols 規範好了,可以去客製化自己所需要的可迭代物件與迭代器確實不錯,但總不能每次都讓開發者去自定義,有個 Javascript 原生提供的工具才合理吧?!
接下來要介紹的 function* 與 Generator 正是為此而存在的。
這兩個也是在 ES6 同時推出的新內容,開發者在 function* 裡頭定義一組生成器函式( Generator function ),這組函式在呼叫後 『並不會執行函式內容更不會返回函式運算結果,而是回傳一個生成器( Generator )物件』 。

生成器函式裡提供了 yield keyword ,開發者可以透過它暫停函式的執行,並會將其右手邊的表達式結果當作 Iterator protocol 中定義的 value 的值回傳出去,而生成器物件同時符合了  Iterable protocol 以及 Iterator protocol 也因此在呼叫了生成器函式取得生成器物件後便擁有了 next method 可以使用:
function* generatorFn() {
	yield 'First Yielding!!';
	yield 'Second Yielding!!';
	yield 'Third Yielding!!';
}
var generator = generatorFn();
generatorFn().next(); // Object { value: 'First Yielding!!', done: false }
generatorFn().next(); // Object { value: 'Second Yielding!!', done: false }
generatorFn().next(); // Object { value: 'Third Yielding!!', done: false }
每執行一次 next method 就會重啟一次函式的執行,直到遇到下一次的 yield keyword 回傳結果,除非遇到:
return keyword 設 value 為右方表達式結果以及將 done 設為 true
value 設為 undefined 以及將 done 設為 true。也就是說 Generator function 是分段執行的,『 yield keyword 負責暫停與句的執行, next method 則會恢復函式的執行』。
除了 next method 以外它也提供 return 、 throw 等 methods 。
next
除了重啟函式的執行之外,如果傳變數進去,則會成為當前重啟的 yield 表達式本身的回傳結果:
function* generatorFn() {
	let test = yield 'yield';
	console.log('test-->', test);
}
var generator = generatorFn();
console.log(generator.next()); // Object { value: 'yield', done: false }
console.log(generator.next('testing!!')); // test-->testing!! \n Object { value: undefined, done: true }
return
直接返回提供給 method 的參數內容作為 value 並設 done 為 true ,並且不會接著繼續執行 Generator function 內容:
function* generatorFn() {
    let test = yield 'yield';
    console.log('test-->n', test);
}
var generator = generatorFn();
console.log(generator.next()); // Object { value: 'yield', done: false }
console.log(generator.return('return value')); // Object { value: 'return value', done: true }
throw
用於向 Generator 內部拋出 Error :
function* generatorFn() {
  while(true) {
    try {
       yield 42;
    } catch(e) {
      console.log("Error caught!");
    }
  }
}
var generator = generatorFn();
generator.next(); // { value: 42, done: false }
generator.throw(new Error()); // "Error caught!"
以上就是對 JS Iteration 的事前介紹,讀者也可以前往 MDN 查看裡頭的介紹,這裡再另外提供一些資源給讀者:
緊接著就到 slate 裡頭是如何搭配 Iteration 去實作遍歷功能的。
只要是與遍歷相關的功能, slate 都是透過 generator 以及 for...of statement 來實作的,它同時定義了幾組 Entry types 作為透過 Generator 執行遍歷功能時 yield keyword 回傳的 type ,分別是 NodeEntry 、 ElementEntry 、 PointEntry ,例如當 Generator 要透過 yield 回傳 element value 時, slate 會選擇回傳 ElementEntry 而非 Element type
export interface NodeInterface {
	elements: (
    root: Node,
    options?: {
      from?: Path
      to?: Path
      reverse?: boolean
      pass?: (node: NodeEntry) => boolean
    }
  ) => Generator<ElementEntry, void, undefined>
}
來看一下這三種 Entry types 的 type 是怎麼被定義的吧!首先從 NodeEntry 與 ElementEntry 開始:
/**
 * `NodeEntry` objects are returned when iterating over the nodes in a Slate
 * document tree. They consist of the node and its `Path` relative to the root
 * node in the document.
 */
export type NodeEntry<T extends Node = Node> = [T, Path]
/**
 * `ElementEntry` objects refer to an `Element` and the `Path` where it can be
 * found inside a root node.
 */
export type ElementEntry = [Element, Path]
這兩組 Entry types 本質跟用法上都是相似的,差別就只是在 NodeEntry 的使用範圍比較廣而 ElementEntry 僅限縮在 element 的上,筆者大多時候都是使用 NodeEntry 來做遍歷,然後在搭配 match method option 與 statement control 去處理不同 type 的情境。
match method 可以在許多 method apis ,包含 Transform methods 以及 Operations 裡看到它被放在 options 裡面,隨便拿一個 transforms/node.ts 裡的 insertNodes 當作範例:

搭配著 NodeMatch type 的它,用途就是提供給開發者一個 narrow Node type 的 helper method ,來看一下 NodeMatch type 裡面的定義就會非常清楚了:
/**
 * A helper type for narrowing matched nodes with a predicate.
 */
export type NodeMatch<T extends Node> =
  | ((node: Node, path: Path) => node is T)
  | ((node: Node, path: Path) => boolean)
接著是最後的 PointEntry ,它的用途就很限縮了,作者留給它的 comment 介紹就說明的非常直白了:它就是拿來 Iterate 一組 range 裡頭的 anchor 與 focus value
/**
 * `PointEntry` objects are returned when iterating over `Point` objects that
 * belong to a range.
 */
export type PointEntry = [Point, 'anchor' | 'focus']
在 slate 裡僅有 interfaces/range 裡頭的 points method api 使用到 PointEntry 而已,其實它就是去實作 PointEntry 的 comment 描述的功能而已:
/**
 * Iterate through all of the point entries in a range.
 */
*points(range: Range): Generator<PointEntry, void, undefined> {
  yield [range.anchor, 'anchor']
  yield [range.focus, 'focus']
},
來做個統整刷刷存在感好了,今天都沒有我出場的機會。
今天首先從 JS Iterate 的 Protocols 開始介紹起,解釋了Iterable protocol與Iterator protocol之間的差異更了解如何實作出一組同時符合者兩個 Protocols 的JS Object。
接著輪到了 Generator ,我們探討了yield這個 keyword 的使用方式以及 Generator 提供的各種 methods 的使用情境。
最後輪到了各種 Entry types 的介紹,再順便了解matchoption method 搭配NodeMatchtype 的作用。
緊接著下一篇就要為 Interface 這個章節收尾了!
下一篇要探討的主題是 slate 的 Custom type ,在準備這篇的內容時筆者是非常興奮,最後也獲益良多的!因為它主要的 code 只有短短不到 10 行而已,卻獨自包攬了 slate 所有的 custom types 定義的功能,真的非常之厲害!
我們也會從它的歷史小故事開始介紹起,明天再見吧~